package com.luobo.shiro.config;

import com.alibaba.fastjson.JSON;
import com.luobo.shiro.bean.Result;
import com.luobo.shiro.service.impl.RedisService;
import com.luobo.shiro.util.IPUtil;
import com.luobo.shiro.util.JWTUtil;
import io.jsonwebtoken.Claims;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.Objects;

/**
 * 此方案实现了控制并发登录,限制用户同时只能在一处登录
 * 并且校验token ip 防止token被盗用 (缺点: 不能控制并发登录数量)
 * @author : AnWen
 * @version :1.0
 * @email : anwen375@qq.com
 * @since : 2020/2/21 10:49
 */
public class SessionControlFilter extends FormAuthenticationFilter {

    private RedisService redisService;
    /**
     *  shiro 用户同步记录 在redis中的key
     */
    private String prefix = "shiro:username:";

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        return false;
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest servletRequest = WebUtils.toHttp(request);
        HttpServletResponse servletResponse = WebUtils.toHttp(response);
        String ipAddr = IPUtil.getIpAddr(servletRequest);
        String token = servletRequest.getHeader(MySessionManager.TOKEN_NAME);
        Claims claims = JWTUtil.parseSessionId(token);
        String ip = "";
        Subject subject = getSubject(request, response);
        if (claims != null) {
            ip = claims.getIssuer();
            Object username = claims.get("username"); //token中的username
            Object redisToken = redisService.get("username:" + username);//缓存中对应的用户存储的token
            if (!token.equals(redisToken)) {
                //如果redis中时null 那么存储一份
                if (Objects.isNull(redisToken)) {
                    redisService.set(prefix + username, token);
                } else {
                    //如果不为空 取出token中的创建时间 和 redisToken中的创建时间
                    Long tokenTime = claims.get("createTime", Long.class);
                    Long redisTime = JWTUtil.parseSessionId((String) redisToken).get("createTime", Long.class);
                    // token > redisToken 则覆盖
                    if (tokenTime.compareTo(redisTime) > 0) {
                        redisService.set(prefix + username, token);
                    } else {
                        // 注销当前token
                        subject.logout();
                        servletResponse.setStatus(402);
                        writeMessage(servletResponse, "下线通知：当前账号另一地点登录， 您被迫下线。若非本人操作，您的登录密码很可能已经泄露，请及时改密。");
                        return false;
                    }
                }
            }
        }else{
            servletResponse.setStatus(401);
            writeMessage(servletResponse, "token无效，请重新登录");
        }

        boolean flag = false;
        if (ipAddr.equals(ip)) {
            flag = true;
        }

        //如果登录了，并且ip是安全的那么 直接进行之后的流程
        if ((subject.isAuthenticated() || subject.isRemembered()) && flag) {
            return true;
        }
        subject.logout();
        servletResponse.setStatus(401);
        writeMessage(servletResponse, "token无效，请重新登录");
        return false;
    }

    protected void writeMessage(HttpServletResponse response, String message) throws Exception {
        response.setContentType("application/json;charset=utf-8");
        PrintWriter writer = response.getWriter();
        writer.println(JSON.toJSONString(Result.error(message)));
        writer.flush();
        writer.close();
    }

    public void setRedisService(RedisService redisService) {
        this.redisService = redisService;
    }

    public RedisService getRedisService() {
        return redisService;
    }

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }
}
